package pseudoAssemblerCompiler.twopassassembler.model.assemblerAlgorithms;

import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import pseudoAssemblerCompiler.twopassassembler.common.NotificationCenter;
import pseudoAssemblerCompiler.twopassassembler.controller.Constants;
import pseudoAssemblerCompiler.twopassassembler.exceptions.RequiredDataNotSetException;
import pseudoAssemblerCompiler.twopassassembler.model.Utils;
import pseudoAssemblerCompiler.twopassassembler.model.codeoperationtable.ICodeOperationTableModel;
import pseudoAssemblerCompiler.twopassassembler.model.entities.OperationCode;
import pseudoAssemblerCompiler.twopassassembler.model.entities.ReferenceTableEntity;
import pseudoAssemblerCompiler.twopassassembler.model.referencetable.IReferenceTableModel;
import pseudoAssemblerCompiler.twopassassembler.model.symbolnametable.ISymbolicNamesTableModel;

import java.util.LinkedHashSet;
import java.util.List;
import java.util.Scanner;
import java.util.Set;

/**
 * Основная логика работы ДвухПроходного Ассемблера №3
 *
 * @author Чекашов Р. И.
 */
public class ThirdAssembler extends AbstractAssembler {

    private String _consoleMessage;

    private int _lineOfStartSubProgram = 0;

    private boolean _hasSubProgram = false;

    /**
     * Внешнии имена
     */
    private Set<String> _externalDefinitions;

    /**
     * Внешнии ссылки
     */
    private Set<String> _externalReferences;

    public ThirdAssembler(ICodeOperationTableModel codeOperationTable, ISymbolicNamesTableModel symbolicNamesTable, IReferenceTableModel referenceTable) {
        super(codeOperationTable, symbolicNamesTable, referenceTable);
    }

    @Override
    public String getConsoleMessage() {
        return _consoleMessage;
    }

    public void setConsoleMessage(String consoleMessage) {
        this._consoleMessage = consoleMessage;
    }

    @Override
    public void firstPass(String program) {
        _isFirstPass = true;
        clearBuffers();
        clearCounters();
        _referenceTable.reset();
        _symbolicNamesTable.reset();
        int loadingAddr = 0; //адрес загрузки
        int codeSize; //размер команды
        Scanner scanner = new Scanner(program);
        _addrCounter = 0;
        List<String> param;
        _externalDefinitions = new LinkedHashSet<String>();
        _externalReferences = new LinkedHashSet<String>();

        _addrBuilder = new StringBuilder(6);
        int[] arrTKO;
        OperationCode codeTKO;
        String currentLine;
        String[] addressCodeOperand = new String[3];
        int tempCountToStartSubProgram;

        /*
		 * Обработка первой строки которая ищет директиву СТАРТ и устанавливает адрес загрузки
		 */
        if (scanner.hasNextLine()) {
            if(_hasSubProgram) {
                tempCountToStartSubProgram = _lineOfStartSubProgram - 1;
                while (tempCountToStartSubProgram-- != 0) {
                    currentLine = scanner.nextLine();
                    lineCount++;
                    _programmCodelength += currentLine.length() + System.getProperty("line.separator").length();
                }
            } else {
                _lineOfStartSubProgram = 0;
            }
            currentLine = scanner.nextLine();
            lineCount++;
            param = Utils.getTokens(currentLine);
            _programmCodelength += currentLine.length() + System.getProperty("line.separator").length();
            int amountOfToken = param.size();
            _lineOfStartSubProgram++;

            if (amountOfToken > 1 && !("START".equalsIgnoreCase(param.get(1)) || "CSEC".equalsIgnoreCase(param.get(1)))) {
                length = currentLine.length() + System.getProperty("line.separator").length();
                start = _programmCodelength - length;
                length = currentLine.length();
                notifyPublishers("Программа должна начинаться с [Названия программы start/csec]",
                        Constants.OperationCodeController.ERROR);
                return;
            }
            _addrCounter = loadingAddr; //Записываем в счетчик адресов адрес загрузки

            ReferenceTableEntity entity = new ReferenceTableEntity(param.get(0),
                    param.get(1), "000000", lineCount, _programmCodelength, currentLine);
            _referenceTable.add(entity);
        }

        boolean isProgramBodyCompiling = false;
        boolean isEndOfProgram = false;
        String metka;
        String mnemoniCode;  //Мнемокод из ТКО
        String operand;
        addressCodeOperand = new String[3];

        while (scanner.hasNextLine()) {
            currentLine = scanner.nextLine(); //Пропускаем пустые строки
            lineCount++;
            System.out.println(lineCount);
            _lineOfStartSubProgram++;
            _programmCodelength += currentLine.length() + System.getProperty("line.separator").length();
            if(StringUtils.isEmpty(currentLine)) {
                continue;
            }

            if(isEndOfProgram) { //Директива конца программы была найдена, но файл еще не обработал все строки!
                notifyPublishers("Директива [end] конца программы должна стоять в конце кода программы!", Constants.OperationCodeController.ERROR);
                return;
            }

            param = Utils.getTokens(currentLine);
            int amountOfToken = param.size();

            // Обработка внешних имен и ссылок
            if("extdef".equalsIgnoreCase(param.get(0))) {
                if(isProgramBodyCompiling) {
                    notifyPublishers("Определение внешних имен должно быть до основного кода программы!", Constants.OperationCodeController.ERROR);
                    return;
                }
                for(int i = 1; i < amountOfToken; i++) {
                    if (_externalReferences.contains(param.get(i))) {
                        notifyPublishers(String.format("Внешнее имя [%s] уже объявлено во внешней ссылке!", param.get(i)),
                                Constants.OperationCodeController.ERROR);
                        return;
                    }
                    if (!_externalDefinitions.add(param.get(i))) {
                        notifyPublishers(String.format("Внешнее имя [%s] объявлено дважды!", param.get(i)),
                                Constants.OperationCodeController.ERROR);
                        return;
                    }
                }
                continue;
            }
            if("extref".equalsIgnoreCase(param.get(0))) {
                if(isProgramBodyCompiling) {
                    notifyPublishers("Определение внешних ссылок должно быть до основного кода программы!",
                            Constants.OperationCodeController.ERROR);
                    return;
                }
                for(int i = 1; i < amountOfToken; i++) {
                    if (_externalDefinitions.contains(param.get(i))) {
                        notifyPublishers(String.format("Внешнея ссылка [%s] уже объявлена во внешнем имени!", param.get(i)),
                                Constants.OperationCodeController.ERROR);
                        return;
                    }
                    if (!_externalReferences.add(param.get(i))) {
                        notifyPublishers(String.format("Внешнея ссылка [%s] объявлена дважды!", param.get(i)),
                                Constants.OperationCodeController.ERROR);
                        return;
                    }
                }
                continue;
            }
            // Конец обработки внешних имен и ссылок

            if(3 < amountOfToken) { //В строке содержатся лишние параметры
                notifyPublishers("Строка содержит лишние параметры!", Constants.OperationCodeController.ERROR);
                return;
            }

            //обработка строк с двумя параметрами
            if(amountOfToken == 2 && !("CSEC".equalsIgnoreCase(param.get(1)))) {
                isProgramBodyCompiling = true;
                mnemoniCode = param.get(0);
                operand = param.get(1);
                codeTKO = _codeOperationTable.getCodeOperationTableElements().get(mnemoniCode.toUpperCase());
                if(null == codeTKO) {
                    notifyPublishers(String.format("Команда [%s] не найдена в ТКО!", mnemoniCode),
                            Constants.OperationCodeController.ERROR);
                    if (_hasSubProgram = true) {
                        _hasSubProgram = false;
                    }
                    return;
                }
                if(codeTKO.is_directive()) {
                    notifyPublishers(String.format("Неверное использование директивы!", mnemoniCode),
                            Constants.OperationCodeController.ERROR);
                    return;
                }
                ReferenceTableEntity entity = new ReferenceTableEntity(addrBuild(_addrCounter, 6),
                        addrBuild(codeTKO.get_binaryCode() * 4 + codeTKO.get_typeOfAddressing(), 2),
                        operand, lineCount, _programmCodelength, currentLine);
                _referenceTable.add(entity);
                _addrCounter += codeTKO.get_lengthOfCommand();
                continue;
            }

            //обработка строк с тремя параметрами
            if(amountOfToken == 3) {
                isProgramBodyCompiling = true;
                metka = param.get(0);
                mnemoniCode = param.get(1);
                operand = param.get(2);

                //Проверка на совпадение метки с именем из таблицы внешних ссылок
                if(_externalReferences.contains(metka)) {
                    notifyPublishers(String.format("Метка [%s] уже объявлена во внешних ссылках!", metka),
                            Constants.OperationCodeController.ERROR);
                    return;
                }

                //Проверка на повторное определение переменной в таблице символических имен
                if(_symbolicNamesTable.getSymbolNames().containsKey(metka)) {
                    notifyPublishers(String.format("Повторное объявление метки [%s] в ТСИ!", metka),
                            Constants.OperationCodeController.ERROR);
                    return;
                }

                _symbolicNamesTable.getSymbolNames().put(metka, addrBuild(_addrCounter, 6)); //Заполнение ТСИ

                //Заполнение вспомогательной таблицы
                codeTKO = _codeOperationTable.getCodeOperationTableElements().get(mnemoniCode.toUpperCase());
                if(null == codeTKO) {
                    notifyPublishers(String.format("Команда [%s] не найдена в ТКО!", mnemoniCode),
                            Constants.OperationCodeController.ERROR);
                    return;
                }
                if(codeTKO.is_directive()) {
                    param.set(0, addrBuild(_addrCounter, 6));
                    try{
                        manageDirectiv(new String[]{param.get(1), param.get(2)}, codeTKO, currentLine);
                    } catch (RequiredDataNotSetException ex) {
                        String msg = String.format("Значение %s не является числом или оно слишком большое, или <= нулю!",
                                ex.getMessage());
                        notifyPublishers(msg, Constants.OperationCodeController.ERROR);
                        return;
                    }
                    ReferenceTableEntity entity = new ReferenceTableEntity(param.get(0),
                            param.get(1), param.get(2), lineCount, _programmCodelength, currentLine);
                    _referenceTable.add(entity);
                } else {
                    ReferenceTableEntity entity = new ReferenceTableEntity(addrBuild(_addrCounter, 6),
                            addrBuild(codeTKO.get_binaryCode() * 4 + codeTKO.get_typeOfAddressing(), 2),
                            operand, lineCount, _programmCodelength, currentLine);
                    _referenceTable.add(entity);
                    _addrCounter += codeTKO.get_lengthOfCommand();
                }
                continue;
            }


            if("end".equalsIgnoreCase(param.get(0)) || "CSEC".equalsIgnoreCase(param.get(1))) { //Конец программы
                isEndOfProgram = true;
                ReferenceTableEntity entity = new ReferenceTableEntity(addrBuild(_addrCounter, 6),
                        "END", addrBuild(loadingAddr, 6), lineCount, _programmCodelength, currentLine);
                _referenceTable.add(entity);

                entity = new ReferenceTableEntity("", "", "", lineCount, _programmCodelength, currentLine);
                _referenceTable.add(entity);

                entity = new ReferenceTableEntity("Длина", "модуля",
                        addrBuild(_addrCounter - loadingAddr, 0), lineCount, _programmCodelength, currentLine);
                _referenceTable.add(entity);

                if(amountOfToken > 1 && "CSEC".equalsIgnoreCase(param.get(1))) {
                    _hasSubProgram = true;
                    break;
                } else {
                    _hasSubProgram = false;
                    continue;
                }
            }

            notifyPublishers(String.format("Не известное определение [%s]!", param.get(0)),
                    Constants.OperationCodeController.ERROR);
        }

        if(isEndOfProgram) {
            //назначение внешним именам адресов и проверка на отсутствие безадресных внешних имен
            for(String extDef : _externalDefinitions) {
                if(!_symbolicNamesTable.getSymbolNames().containsKey(extDef.toLowerCase())) {
                    notifyPublishers(String.format("Не определен адрес для внешнего имени [%s]", extDef), Constants.OperationCodeController.ERROR);
                }
            }
            notifyPublishers("Достигнут конец программы.", Constants.OperationCodeController.INFO);
            return;
        } else {
            notifyPublishers(String.format("Не найдена директива окончания программы [%s]", "end"), Constants.OperationCodeController.ERROR);
            return;
        }
    }

    @Override
    public void secondPass() {
        _isFirstPass = false;
        clearBuffers();
        List<ReferenceTableEntity> referenceTable = _referenceTable.getReferenceTableEntities();
        if (referenceTable == null || referenceTable.size() == 0) {
            notifyPublishers(String.format("Вспомогательная таблица пуста referenceTable = %s", referenceTable),
                    Constants.OperationCodeController.ERROR);
            throw new IllegalArgumentException(getConsoleMessage());
        }

//        for (String firstLine : referenceTable.get(0)) {
//            _infoBuffer.append(firstLine + " ");
//        }
//        _infoBuffer.append(referenceTable.get(referenceTable.size() - 1)[2]);
        _infoBuffer.append(referenceTable.get(0).get_address() + " " + referenceTable.get(0).get_code() + " " + referenceTable.get(0).get_operand());
        _infoBuffer.append(" " + referenceTable.get(referenceTable.size() - 1).get_operand());
        _infoBuffer.append(System.getProperty("line.separator"));
        //Конец формирования записи H

        //Формирование записей D
        if(0 < _externalDefinitions.size()) {
            _infoBuffer.append("D ");
            for(String str: _externalDefinitions) {
                _infoBuffer.append(str + " " + _symbolicNamesTable.getSymbolNames().get(str.toLowerCase()) + " ");
            }
            _infoBuffer.append(System.getProperty("line.separator"));
        }
        //Конец формирования записей D

        //Формирование записей R
        if(0 < _externalReferences.size()) {
            _infoBuffer.append("R ");
            for(String str: _externalReferences) {
                _infoBuffer.append(str + " ");
            }
            _infoBuffer.append(System.getProperty("line.separator"));
        }
        //Конец формирования записей R

        String operand;
        String mnemonicCode;
        String metka;
        int symbolCount = 0;

        StringBuilder oper = new StringBuilder();

        // начало формирования записей "T"
//        _buffer.delete(0, _buffer.length());
        for (int i = 1; i < referenceTable.size(); i++) {
            String refTableOperand = referenceTable.get(i).get_operand();
            mnemonicCode = referenceTable.get(i).get_code().toUpperCase();
            if (!"END".equalsIgnoreCase(mnemonicCode)) {
                _infoBuffer.append(referenceTable.get(i).get_address() + " ");
            }
            if (_codeOperationTable.getCodeOperationTableElements().containsKey(mnemonicCode)) {
                try {
                    codeOperationTableContainMnemonicCode(mnemonicCode, referenceTable.get(i));
                } catch (RequiredDataNotSetException e) {
                    if("end".equalsIgnoreCase(e.getMessage())) {
                        _infoBuffer.append(" E " + refTableOperand + System.getProperty("line.separator"));
                        _notificationCenter.postNotification(this, Constants.OperationCodeController.INFO);
                        return;
                    }
                    showError(e.getMessage(), referenceTable.get(i));
                    String msg = String.format("Значение %s не является числом или оно слишком большое, или <= нулю!",
                            refTableOperand);
                    notifyPublishers(msg, Constants.OperationCodeController.ERROR, referenceTable.get(i));
                    LOGGER.error(msg);
                    return;
                }
//                int number;
//                try {
//                    number = Integer.parseInt(referenceTable.get(i)[2]);
//                    if(0 >= number || 99999 < number) {
//                        throw new NumberFormatException(referenceTable.get(i)[2]);
//                    }
//                } catch (NumberFormatException ex) {
//                    String msg = String.format("Значение %s не является числом или оно слишком большое, или <= нулю!",
//                            referenceTable.get(i)[2]);
//                    notifyPublishers(msg, Constants.OperationCodeController.ERROR);
//                    LOGGER.error(msg);
//                    return;
//                }
//                if ("RESB".equalsIgnoreCase(mnemonicCode) || "RESW".equalsIgnoreCase(mnemonicCode)) {
//                    symbolCount = number * _codeOperationTable.getCodeOperationTableElements().get(mnemonicCode).get_lengthOfCommand() * 2;
//                    _infoBuffer.append(Utils.addrBuild(symbolCount, 2) + " ");
//                    while (symbolCount-- > 0) {
//                        oper.append("0");
//                    }
//                    _infoBuffer.append(oper.toString() + System.getProperty("line.separator"));
//                    oper.delete(0, oper.length());
//                } else if("WORD".equalsIgnoreCase(mnemonicCode)) {
//                    symbolCount = 1 * _codeOperationTable.getCodeOperationTableElements().get(mnemonicCode).get_lengthOfCommand() * 2;
//                    _infoBuffer.append(Utils.addrBuild(symbolCount, 2) + " ");
//                    _infoBuffer.append(Utils.addrBuild(number, 6) + System.getProperty("line.separator"));
//                } else if("BYTE".equalsIgnoreCase(mnemonicCode)) {
//                    if ("X".equalsIgnoreCase(referenceTable.get(i)[2].substring(0,1))) {
//                        String subString = referenceTable.get(i)[2].substring(2, referenceTable.get(i)[2].length());
//                        _infoBuffer.append(Utils.addrBuild(subString.length(), 2) +
//                                " " + subString + System.getProperty("line.separator"));
//                    } else if (("C".equalsIgnoreCase(referenceTable.get(i)[2].substring(0,1)))) {
//                        String subString = referenceTable.get(i)[2].substring(2, referenceTable.get(i)[2].length());
//                        for (Character c : subString.toCharArray()) {
//                            oper.append(Utils.addrBuild(c, 2));
//                        }
//                        _infoBuffer.append(Utils.addrBuild(subString.length(), 2) +
//                                " " + oper.toString() + System.getProperty("line.separator"));
//                        oper.delete(0, oper.length());
//                    } else {
//                        String num = Integer.toHexString(number);
//                        _infoBuffer.append(Utils.addrBuild(num.length(), 2) + " " + num + System.getProperty("line.separator"));
//                    }
//                } else if("END".equalsIgnoreCase(mnemonicCode)) {
//                    _infoBuffer.append(" E " + referenceTable.get(i)[2] + System.getProperty("line.separator"));
//                    _notificationCenter.postNotification(this, Constants.OperationCodeController.INFO);
//                    return;
//                } else {
//
//                    _infoBuffer.append(referenceTable.get(i)[1] + " ");
//                    _infoBuffer.append(
//                            ((_symbolicNamesTable.getSymbolNames().get(referenceTable.get(i)[2]) == null) ?
//                                    referenceTable.get(i)[2] : _symbolicNamesTable.getSymbolNames().get(referenceTable.get(i)[2]))
//                                    + System.getProperty("line.separator"));
//                }

            } else {
                operand = _symbolicNamesTable.getSymbolNames().get(refTableOperand);

                if ("R1,R2".equalsIgnoreCase(refTableOperand)) {
                    operand = "12";
                }


                if(null == operand) {
                    if(StringUtils.isEmpty(refTableOperand)) {
                        continue;
                    }

                    String str = refTableOperand;
                    if(str.startsWith("{") && str.endsWith("}")) {
                        str = str.substring(1, str.length() - 1);
                    }
                    if(!_symbolicNamesTable.getSymbolNames().containsKey(str) && !_externalReferences.contains(str)) {
                        showError(str, referenceTable.get(i));
                        String msg = String.format("В ТСИ и во внешних ссылках [%s] не существует", str);
                        _hasSecondPassErrors = true;
                        notifyPublishers(msg, Constants.OperationCodeController.ERROR, referenceTable.get(i));
                        return;
                    }

                    if(_externalReferences != null && _externalReferences.contains(refTableOperand)) {
                        operand = "000000";
                        symbolCount = mnemonicCode.length() + operand.length();
                        _infoBuffer.append(Utils.addrBuild(symbolCount, 2) + " " + mnemonicCode + " " + operand + System.getProperty("line.separator"));
                        continue;
                    }

                    metka = refTableOperand.substring(1, refTableOperand.length() - 1);
                    operand = _symbolicNamesTable.getSymbolNames().get(metka);

                    if(null != operand) {
                        String nextAddr = referenceTable.get(i + 1).get_address();
                        while (operand.charAt(0) == '0') {
                            operand = operand.substring(1);
                        }
                        operand = "0x" + operand;
                        while (nextAddr.charAt(0) == '0') {
                            nextAddr = nextAddr.substring(1);
                        }
                        nextAddr = "0x" + nextAddr;

                        operand = Utils.addrBuild(Integer.decode(operand) - Integer.decode(nextAddr), 6);
                        if(operand.length() > 6) {
                            operand = operand.substring(operand.length() - 6);
                        }
                        symbolCount = mnemonicCode.length() + operand.length();
                        _infoBuffer.append(Utils.addrBuild(symbolCount, 2) + " " + mnemonicCode + " " + operand + System.getProperty("line.separator"));

                        int len;
                        for(ReferenceTableEntity entity : referenceTable) {
                            if(_symbolicNamesTable.getKeys().contains(entity.get_operand())) {
                                _infoBuffer.append("M " + entity.get_address() + System.getProperty("line.separator"));
                            }
                            if(_externalReferences.contains(entity.get_operand())) {
                                _infoBuffer.append("M " + entity.get_address() + " " + entity.get_operand() + System.getProperty("line.separator"));
                            }
                        }
                    }
                } else {
                    symbolCount = mnemonicCode.length() + operand.length();
                    _infoBuffer.append(Utils.addrBuild(symbolCount, 2) + " " + mnemonicCode + " " + operand + System.getProperty("line.separator"));
                }
            }
        }
    }

}